<?php


namespace lib;



class RedisLock {
    /**
     * redis key前缀
     */
    const REDIS_LOCK_KEY_PREFIX = 'redis:lock:';

    /**
     * @var array
     */
    private $lockedNames = [];

    /**
     * @var \Redis
     */
    private $redisObject = null;

    /**
     * @var null
     */
    private static $self = null;

    /**
     *
     */
    const REDIS_LOCK_PREFIX = 'redis:lock:';

    /**
     * RedisLock constructor.
     */
    private function __construct() {
    }

    /**
     *
     */
    private function __clone() {

    }

    /**
     * 获取锁对象
     *
     * @param \Redis $redisObject
     *
     * @return null|RedisLock
     */
    public static function getInstance($redisObject) {

        if (self::$self == null) {
            $self = new self();
            $self->redisObject = $redisObject;
            self::$self = $self;
        }
        return self::$self;
    }

    /**
     *
     * @param string $name
     * @param int    $expire
     * @param int    $retryTimes
     * @param int    $sleep
     *
     * @return bool|mixed
     * @author : Randy_chen
     * @Date   : 2019/9/2
     * @Time   : 9:55
     */
    public function lock(string $name, $expire = 5, $retryTimes = 10, $sleep = 10000) {
        $flag = false;
        $retryTimes = max($retryTimes, 1);
        $key = self::REDIS_LOCK_KEY_PREFIX . $name;
        while ($retryTimes-- > 0) {
            $kVal = microtime(true) + $expire;
            $flag = $this->getLock($key, $expire, $kVal);//上锁
            if ($flag) {
                $this->lockedNames[$key] = $kVal;
                break;
            }
            usleep($sleep);
        }
        return $flag;
    }

    /**
     * 获取锁
     *
     * @param $key
     * @param $expire
     * @param $value
     *
     * @return mixed
     */
    private function getLock($key, $expire, $value) {
        $script = <<<LUA
            local key = KEYS[1]
            local value = ARGV[1]
            local ttl = ARGV[2]

            if (redis.call('setnx', key, value) == 1) then
                return redis.call('expire', key, ttl)
            elseif (redis.call('ttl', key) == -1) then
                return redis.call('expire', key, ttl)
            end
            
            return 0
LUA;
        return $this->execLuaScript($script, [
            $key,
            $value,
            $expire
        ]);
    }

    /**
     * 解锁
     *
     * @param string $name
     *
     * @return mixed
     */
    public function unlock(string $name) {
        $script = <<<LUA
            local key = KEYS[1]
            local value = ARGV[1]

            if (redis.call('exists', key) == 1 and redis.call('get', key) == value) 
            then
                return redis.call('del', key)
            end

            return 0
LUA;
        $key = self::REDIS_LOCK_KEY_PREFIX . $name;
        if (isset($this->lockedNames[$key])) {
            $val = $this->lockedNames[$key];
            return $this->execLuaScript($script, [
                $key,
                $val
            ]);
        }
        return false;
    }

    /**
     * 执行lua脚本
     *
     * @param string $script
     * @param array  $params
     * @param int    $keyNum
     *
     * @return mixed
     */
    private function execLuaScript($script, array $params, $keyNum = 1) {
        $hash = $this->redisObject->script('load', $script);
        return $this->redisObject->evalSha($hash, $params, $keyNum);
    }

    /**
     * 获取锁并执行
     *
     * @param callable $func
     * @param string   $name
     * @param int      $expire
     * @param int      $retryTimes
     * @param int      $sleep
     *
     * @return bool
     * @throws \Exception
     */
    public function run(callable $func, string $name, $expire = 5, $retryTimes = 10, $sleep = 10000) {
        if ($this->lock($name, $expire, $retryTimes, $sleep)) {
            try {
                call_user_func($func);
            } catch (\Exception $e) {
                throw $e;
            } finally {
                $this->unlock($name);
            }
            return true;
        } else {
            return false;
        }
    }

}